package org.recentwidget.compat.gmail;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.recentwidget.compat.util.Lists;
import org.recentwidget.compat.util.Maps;
import org.recentwidget.compat.util.Sets;

import android.content.AsyncQueryHandler;
import android.content.ContentQueryMap;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.BaseColumns;
import android.text.Html;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextUtils.SimpleStringSplitter;
import android.text.style.CharacterStyle;
import android.util.Log;

/**
 * Taken from
 * http://www.google.co.in/codesearch/p?hl=en#uX1GffpyOZk/core/java/android
 * /provider/Gmail.java&q=gmail.java&d=0
 * 
 */
public class Gmail {
	// Set to true to enable extra debugging.
	private static final boolean DEBUG = false;

	public static final String GMAIL_AUTH_SERVICE = "mail";
	// These constants come from
	// google3/java/com/google/caribou/backend/MailLabel.java.
	public static final String LABEL_SENT = "^f";
	public static final String LABEL_INBOX = "^i";
	public static final String LABEL_DRAFT = "^r";
	public static final String LABEL_UNREAD = "^u";
	public static final String LABEL_TRASH = "^k";
	public static final String LABEL_SPAM = "^s";
	public static final String LABEL_STARRED = "^t";
	public static final String LABEL_CHAT = "^b"; // 'b' for 'buzz'
	public static final String LABEL_VOICEMAIL = "^vm";
	public static final String LABEL_IGNORED = "^g";
	public static final String LABEL_ALL = "^all";
	// These constants (starting with "^^") are only used locally and are not
	// understood by the
	// server.
	public static final String LABEL_VOICEMAIL_INBOX = "^^vmi";
	public static final String LABEL_CACHED = "^^cached";
	public static final String LABEL_OUTBOX = "^^out";

	public static final String AUTHORITY = "gmail-ls";
	private static final String TAG = "Gmail";
	public static final String AUTHORITY_PLUS_CONVERSATIONS = "content://"
			+ AUTHORITY + "/conversations/";
	private static final String AUTHORITY_PLUS_LABELS = "content://"
			+ AUTHORITY + "/labels/";
	public static final String AUTHORITY_PLUS_MESSAGES = "content://"
			+ AUTHORITY + "/messages/";
	private static final String AUTHORITY_PLUS_SETTINGS = "content://"
			+ AUTHORITY + "/settings/";

	public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
	private static final Uri LABELS_URI = Uri.parse(AUTHORITY_PLUS_LABELS);
	public static final Uri CONVERSATIONS_URI = Uri
			.parse(AUTHORITY_PLUS_CONVERSATIONS);
	private static final Uri SETTINGS_URI = Uri.parse(AUTHORITY_PLUS_SETTINGS);

	/** Separates email addresses in strings in the database. */
	public static final String EMAIL_SEPARATOR = "\n";
	public static final Pattern EMAIL_SEPARATOR_PATTERN = Pattern
			.compile(EMAIL_SEPARATOR);

	/**
	 * Space-separated lists have separators only between items.
	 */
	private static final char SPACE_SEPARATOR = ' ';
	public static final Pattern SPACE_SEPARATOR_PATTERN = Pattern.compile(" ");

	/**
	 * Comma-separated lists have separators between each item, before the first
	 * and after the last item. The empty list is <tt>,</tt>.
	 * 
	 * <p>
	 * This makes them easier to modify with SQL since it is not a special case
	 * to add or remove the last item. Having a separator on each side of each
	 * value also makes it safe to use SQL's REPLACE to remove an item from a
	 * string by using REPLACE(',value,', ',').
	 * 
	 * <p>
	 * We could use the same separator for both lists but this makes it easier
	 * to remember which kind of list one is dealing with.
	 */
	private static final char COMMA_SEPARATOR = ',';
	public static final Pattern COMMA_SEPARATOR_PATTERN = Pattern.compile(",");

	/** Separates attachment info parts in strings in the database. */
	public static final String ATTACHMENT_INFO_SEPARATOR = "\n";
	public static final Pattern ATTACHMENT_INFO_SEPARATOR_PATTERN = Pattern
			.compile(ATTACHMENT_INFO_SEPARATOR);

	public static final Character SENDER_LIST_SEPARATOR = '\n';
	public static final String SENDER_LIST_TOKEN_ELIDED = "e";
	public static final String SENDER_LIST_TOKEN_NUM_MESSAGES = "n";
	public static final String SENDER_LIST_TOKEN_NUM_DRAFTS = "d";
	public static final String SENDER_LIST_TOKEN_LITERAL = "l";
	public static final String SENDER_LIST_TOKEN_SENDING = "s";
	public static final String SENDER_LIST_TOKEN_SEND_FAILED = "f";

	/** Used for finding status in a cursor's extras. */
	public static final String EXTRA_STATUS = "status";

	public static final String RESPOND_INPUT_COMMAND = "command";
	public static final String COMMAND_RETRY = "retry";
	public static final String COMMAND_ACTIVATE = "activate";
	public static final String COMMAND_SET_VISIBLE = "setVisible";
	public static final String SET_VISIBLE_PARAM_VISIBLE = "visible";
	public static final String RESPOND_OUTPUT_COMMAND_RESPONSE = "commandResponse";
	public static final String COMMAND_RESPONSE_OK = "ok";
	public static final String COMMAND_RESPONSE_UNKNOWN = "unknownCommand";

	public static final String INSERT_PARAM_ATTACHMENT_ORIGIN = "origin";
	public static final String INSERT_PARAM_ATTACHMENT_ORIGIN_EXTRAS = "originExtras";

	private static final Pattern NAME_ADDRESS_PATTERN = Pattern
			.compile("\"(.*)\"");
	private static final Pattern UNNAMED_ADDRESS_PATTERN = Pattern
			.compile("([^<]+)@");

	private static final Map<Integer, Integer> sPriorityToLength = new HashMap<Integer, Integer>();
	public static final SimpleStringSplitter sSenderListSplitter = new SimpleStringSplitter(
			SENDER_LIST_SEPARATOR);
	public static String[] sSenderFragments = new String[8];

	public static final Pattern EMAIL_ADDRESS_PATTERN = Pattern
			.compile("[a-zA-Z0-9\\+\\.\\_\\%\\-]{1,256}" + "\\@"
					+ "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + "(" + "\\."
					+ "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + ")+");

	/**
	 * Returns the name in an address string
	 * 
	 * @param addressString
	 *            such as &quot;bobby&quot; &lt;bob@example.com&gt;
	 * @return returns the quoted name in the addressString, otherwise the
	 *         username from the email address
	 */
	public static String getNameFromAddressString(String addressString) {
		Matcher namedAddressMatch = NAME_ADDRESS_PATTERN.matcher(addressString);
		if (namedAddressMatch.find()) {
			String name = namedAddressMatch.group(1);
			if (name.length() > 0)
				return name;
			addressString = addressString.substring(namedAddressMatch.end(),
					addressString.length());
		}

		Matcher unnamedAddressMatch = UNNAMED_ADDRESS_PATTERN
				.matcher(addressString);
		if (unnamedAddressMatch.find()) {
			return unnamedAddressMatch.group(1);
		}

		return addressString;
	}

	/**
	 * Returns the email address in an address string
	 * 
	 * @param addressString
	 *            such as &quot;bobby&quot; &lt;bob@example.com&gt;
	 * @return returns the email address, such as bob@example.com from the
	 *         example above
	 */
	public static String getEmailFromAddressString(String addressString) {
		String result = addressString;
		Matcher match = EMAIL_ADDRESS_PATTERN.matcher(addressString);
		if (match.find()) {
			result = addressString.substring(match.start(), match.end());
		}

		return result;
	}

	/**
	 * Returns whether the label is user-defined (versus system-defined labels
	 * such as inbox, whose names start with "^").
	 */
	public static boolean isLabelUserDefined(String label) {
		// TODO: label should never be empty so we should be able to say
		// [label.charAt(0) != '^'].
		// However, it's a release week and I'm too scared to make that change.
		return !label.startsWith("^");
	}

	private static final Set<String> USER_SETTABLE_BUILTIN_LABELS = Sets
			.newHashSet(Gmail.LABEL_INBOX, Gmail.LABEL_UNREAD,
					Gmail.LABEL_TRASH, Gmail.LABEL_SPAM, Gmail.LABEL_STARRED,
					Gmail.LABEL_IGNORED);

	/**
	 * Returns whether the label is user-settable. For example, labels such as
	 * LABEL_DRAFT should only be set internally.
	 */
	public static boolean isLabelUserSettable(String label) {
		return USER_SETTABLE_BUILTIN_LABELS.contains(label)
				|| isLabelUserDefined(label);
	}

	/**
	 * Returns the set of labels using the raw labels from a previous
	 * getRawLabels() as input.
	 * 
	 * @return a copy of the set of labels. To add or remove labels call
	 *         MessageCursor.addOrRemoveLabel on each message in the
	 *         conversation.
	 */
	public static Set<Long> getLabelIdsFromLabelIdsString(
			TextUtils.StringSplitter splitter) {
		Set<Long> labelIds = Sets.newHashSet();
		for (String labelIdString : splitter) {
			labelIds.add(Long.valueOf(labelIdString));
		}
		return labelIds;
	}

	/**
	 * @deprecated remove when the activities stop using canonical names to
	 *             identify labels
	 */
	@Deprecated
	public static Set<String> getCanonicalNamesFromLabelIdsString(
			LabelMap labelMap, TextUtils.StringSplitter splitter) {
		Set<String> canonicalNames = Sets.newHashSet();
		for (long labelId : getLabelIdsFromLabelIdsString(splitter)) {
			final String canonicalName = labelMap.getCanonicalName(labelId);
			// We will sometimes see labels that the label map does not yet know
			// about or that
			// do not have names yet.
			if (!TextUtils.isEmpty(canonicalName)) {
				canonicalNames.add(canonicalName);
			} else {
				Log.w(TAG,
						"getCanonicalNamesFromLabelIdsString skipping label id: "
								+ labelId);
			}
		}
		return canonicalNames;
	}

	/**
	 * @return a StringSplitter that is configured to split message label id
	 *         strings
	 */
	public static TextUtils.StringSplitter newMessageLabelIdsSplitter() {
		return new TextUtils.SimpleStringSplitter(SPACE_SEPARATOR);
	}

	/**
	 * @return a StringSplitter that is configured to split conversation label
	 *         id strings
	 */
	public static TextUtils.StringSplitter newConversationLabelIdsSplitter() {
		return new CommaStringSplitter();
	}

	/**
	 * A splitter for strings of the form described in the docs for
	 * COMMA_SEPARATOR.
	 */
	private static class CommaStringSplitter extends
			TextUtils.SimpleStringSplitter {

		public CommaStringSplitter() {
			super(COMMA_SEPARATOR);
		}

		@Override
		public void setString(String string) {
			// The string should always be at least a single comma.
			super.setString(string.substring(1));
		}
	}

	/**
	 * Creates a single string of the form that getLabelIdsFromLabelIdsString
	 * can split.
	 */
	public static String getLabelIdsStringFromLabelIds(Set<Long> labelIds) {
		StringBuilder sb = new StringBuilder();
		sb.append(COMMA_SEPARATOR);
		for (Long labelId : labelIds) {
			sb.append(labelId);
			sb.append(COMMA_SEPARATOR);
		}
		return sb.toString();
	}

	public static final class ConversationColumns {
		public static final String ID = "_id";
		public static final String SUBJECT = "subject";
		public static final String SNIPPET = "snippet";
		public static final String FROM = "fromAddress";
		public static final String DATE = "date";
		public static final String PERSONAL_LEVEL = "personalLevel";
		/**
		 * A list of label names with a space after each one (including the last
		 * one). This makes it easier remove individual labels from this list
		 * using SQL.
		 */
		public static final String LABEL_IDS = "labelIds";
		public static final String NUM_MESSAGES = "numMessages";
		public static final String MAX_MESSAGE_ID = "maxMessageId";
		public static final String HAS_ATTACHMENTS = "hasAttachments";
		public static final String HAS_MESSAGES_WITH_ERRORS = "hasMessagesWithErrors";
		public static final String FORCE_ALL_UNREAD = "forceAllUnread";

		private ConversationColumns() {
		}
	}

	public static final class MessageColumns {

		public static final String ID = "_id";
		public static final String MESSAGE_ID = "messageId";
		public static final String CONVERSATION_ID = "conversation";
		public static final String SUBJECT = "subject";
		public static final String SNIPPET = "snippet";
		public static final String FROM = "fromAddress";
		public static final String TO = "toAddresses";
		public static final String CC = "ccAddresses";
		public static final String BCC = "bccAddresses";
		public static final String REPLY_TO = "replyToAddresses";
		public static final String DATE_SENT_MS = "dateSentMs";
		public static final String DATE_RECEIVED_MS = "dateReceivedMs";
		public static final String LIST_INFO = "listInfo";
		public static final String PERSONAL_LEVEL = "personalLevel";
		public static final String BODY = "body";
		public static final String EMBEDS_EXTERNAL_RESOURCES = "bodyEmbedsExternalResources";
		public static final String LABEL_IDS = "labelIds";
		public static final String JOINED_ATTACHMENT_INFOS = "joinedAttachmentInfos";
		public static final String ERROR = "error";
		// TODO: add a method for accessing this
		public static final String REF_MESSAGE_ID = "refMessageId";

		// Fake columns used only for saving or sending messages.
		public static final String FAKE_SAVE = "save";
		public static final String FAKE_REF_MESSAGE_ID = "refMessageId";

		private MessageColumns() {
		}
	}

	public static final class LabelColumns {
		public static final String CANONICAL_NAME = "canonicalName";
		public static final String NAME = "name";
		public static final String NUM_CONVERSATIONS = "numConversations";
		public static final String NUM_UNREAD_CONVERSATIONS = "numUnreadConversations";

		private LabelColumns() {
		}
	}

	public static final class SettingsColumns {
		public static final String LABELS_INCLUDED = "labelsIncluded";
		public static final String LABELS_PARTIAL = "labelsPartial";
		public static final String CONVERSATION_AGE_DAYS = "conversationAgeDays";
		public static final String MAX_ATTACHMENET_SIZE_MB = "maxAttachmentSize";
	}

	/**
	 * These flags can be included as Selection Arguments when querying the
	 * provider.
	 */
	public static class SelectionArguments {
		private SelectionArguments() {
			// forbid instantiation
		}

		/**
		 * Specifies that you do NOT wish the returned cursor to become the
		 * Active Network Cursor. If you do not include this flag as a
		 * selectionArg, the new cursor will become the Active Network Cursor by
		 * default.
		 */
		public static final String DO_NOT_BECOME_ACTIVE_NETWORK_CURSOR = "SELECTION_ARGUMENT_DO_NOT_BECOME_ACTIVE_NETWORK_CURSOR";
	}

	// These are the projections that we need when getting cursors from the
	// content provider.
	public static String[] CONVERSATION_PROJECTION = { ConversationColumns.ID,
			ConversationColumns.SUBJECT, ConversationColumns.SNIPPET,
			ConversationColumns.FROM, ConversationColumns.DATE,
			ConversationColumns.PERSONAL_LEVEL, ConversationColumns.LABEL_IDS,
			ConversationColumns.NUM_MESSAGES,
			ConversationColumns.MAX_MESSAGE_ID,
			ConversationColumns.HAS_ATTACHMENTS,
			ConversationColumns.HAS_MESSAGES_WITH_ERRORS,
			ConversationColumns.FORCE_ALL_UNREAD };
	public static String[] MESSAGE_PROJECTION = { MessageColumns.ID,
			MessageColumns.MESSAGE_ID, MessageColumns.CONVERSATION_ID,
			MessageColumns.SUBJECT, MessageColumns.SNIPPET,
			MessageColumns.FROM, MessageColumns.TO, MessageColumns.CC,
			MessageColumns.BCC, MessageColumns.REPLY_TO,
			MessageColumns.DATE_SENT_MS, MessageColumns.DATE_RECEIVED_MS,
			MessageColumns.LIST_INFO, MessageColumns.PERSONAL_LEVEL,
			MessageColumns.BODY, MessageColumns.EMBEDS_EXTERNAL_RESOURCES,
			MessageColumns.LABEL_IDS, MessageColumns.JOINED_ATTACHMENT_INFOS,
			MessageColumns.ERROR };
	private static String[] LABEL_PROJECTION = { BaseColumns._ID,
			LabelColumns.CANONICAL_NAME, LabelColumns.NAME,
			LabelColumns.NUM_CONVERSATIONS,
			LabelColumns.NUM_UNREAD_CONVERSATIONS };
	private static String[] SETTINGS_PROJECTION = {
			SettingsColumns.LABELS_INCLUDED, SettingsColumns.LABELS_PARTIAL,
			SettingsColumns.CONVERSATION_AGE_DAYS,
			SettingsColumns.MAX_ATTACHMENET_SIZE_MB, };

	private final ContentResolver mContentResolver;

	public Gmail(ContentResolver contentResolver) {
		mContentResolver = contentResolver;
	}

	/**
	 * Returns source if source is non-null. Returns the empty string otherwise.
	 */
	private static String toNonnullString(String source) {
		if (source == null) {
			return "";
		} else {
			return source;
		}
	}

	/**
	 * Behavior for a new cursor: should it become the Active Network Cursor?
	 * This could potentially lead to bad behavior if someone else is using the
	 * Active Network Cursor, since theirs will stop being the Active Network
	 * Cursor.
	 */
	public static enum BecomeActiveNetworkCursor {
		/**
		 * The new cursor should become the one and only Active Network Cursor.
		 * Any other cursor that might already be the Active Network Cursor will
		 * cease to be so.
		 */
		YES,

		/**
		 * The new cursor should not become the Active Network Cursor. Any other
		 * cursor that might already be the Active Network Cursor will continue
		 * to be so.
		 */
		NO
	}

	/**
	 * Wraps a Cursor in a ConversationCursor
	 * 
	 * @param account
	 *            the account the cursor is associated with
	 * @param cursor
	 *            The Cursor to wrap
	 * @return a new ConversationCursor
	 */
	public ConversationCursor getConversationCursorForCursor(String account,
			Cursor cursor) {
		if (TextUtils.isEmpty(account)) {
			throw new IllegalArgumentException("account is empty");
		}
		return new ConversationCursor(this, account, cursor);
	}

	/**
	 * Creates an array of SelectionArguments suitable for passing to the
	 * provider's query. Currently this only handles one flag, but it could be
	 * expanded in the future.
	 */
	private static String[] getSelectionArguments(
			BecomeActiveNetworkCursor becomeActiveNetworkCursor) {
		if (BecomeActiveNetworkCursor.NO == becomeActiveNetworkCursor) {
			return new String[] { SelectionArguments.DO_NOT_BECOME_ACTIVE_NETWORK_CURSOR };
		} else {
			// Default behavior; no args required.
			return null;
		}
	}

	/**
	 * Asynchronously gets a cursor over all conversations matching a query. The
	 * query is in Gmail's query syntax. When the operation is complete the
	 * handler's onQueryComplete() method is called with the resulting Cursor.
	 * 
	 * @param account
	 *            run the query on this account
	 * @param handler
	 *            An AsyncQueryHanlder that will be used to run the query
	 * @param token
	 *            The token to pass to startQuery, which will be passed back to
	 *            onQueryComplete
	 * @param query
	 *            a query in Gmail's query syntax
	 * @param becomeActiveNetworkCursor
	 *            whether or not the returned cursor should become the Active
	 *            Network Cursor
	 */
	public void runQueryForConversations(String account,
			AsyncQueryHandler handler, int token, String query,
			BecomeActiveNetworkCursor becomeActiveNetworkCursor) {
		if (TextUtils.isEmpty(account)) {
			throw new IllegalArgumentException("account is empty");
		}
		String[] selectionArgs = getSelectionArguments(becomeActiveNetworkCursor);
		handler.startQuery(token, null, Uri.withAppendedPath(CONVERSATIONS_URI,
				account), CONVERSATION_PROJECTION, query, selectionArgs, null);
	}

	/**
	 * Synchronously gets a cursor over all conversations matching a query. The
	 * query is in Gmail's query syntax.
	 * 
	 * @param account
	 *            run the query on this account
	 * @param query
	 *            a query in Gmail's query syntax
	 * @param becomeActiveNetworkCursor
	 *            whether or not the returned cursor should become the Active
	 *            Network Cursor
	 */
	public ConversationCursor getConversationCursorForQuery(String account,
			String query, BecomeActiveNetworkCursor becomeActiveNetworkCursor) {
		String[] selectionArgs = getSelectionArguments(becomeActiveNetworkCursor);
		Cursor cursor = mContentResolver.query(Uri.withAppendedPath(
				CONVERSATIONS_URI, account), CONVERSATION_PROJECTION, query,
				selectionArgs, null);
		return new ConversationCursor(this, account, cursor);
	}

	/**
	 * Gets a message cursor over the single message with the given id.
	 * 
	 * @param account
	 *            get the cursor for messages in this account
	 * @param messageId
	 *            the id of the message
	 * @return a cursor over the message
	 */
	public MessageCursor getMessageCursorForMessageId(String account,
			long messageId) {
		if (TextUtils.isEmpty(account)) {
			throw new IllegalArgumentException("account is empty");
		}
		Uri uri = Uri
				.parse(AUTHORITY_PLUS_MESSAGES + account + "/" + messageId);
		Cursor cursor = mContentResolver.query(uri, MESSAGE_PROJECTION, null,
				null, null);
		return new MessageCursor(this, mContentResolver, account, cursor);
	}

	/**
	 * Gets a message cursor over the messages that match the query. Note that
	 * this simply finds all of the messages that match and returns them. It
	 * does not return all messages in conversations where any message matches.
	 * 
	 * @param account
	 *            get the cursor for messages in this account
	 * @param query
	 *            a query in GMail's query syntax. Currently only queries of the
	 *            form [label:<label>] are supported
	 * @return a cursor over the messages
	 */
	public MessageCursor getLocalMessageCursorForQuery(String account,
			String query) {
		if (TextUtils.isEmpty(account)) {
			throw new IllegalArgumentException("account is empty");
		}
		Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/");
		Cursor cursor = mContentResolver.query(uri, MESSAGE_PROJECTION, query,
				null, null);
		return new MessageCursor(this, mContentResolver, account, cursor);
	}

	/**
	 * Gets a cursor over all of the messages in a conversation.
	 * 
	 * @param account
	 *            get the cursor for messages in this account
	 * @param conversationId
	 *            the id of the converstion to fetch messages for
	 * @return a cursor over messages in the conversation
	 */
	public MessageCursor getMessageCursorForConversationId(String account,
			long conversationId) {
		if (TextUtils.isEmpty(account)) {
			throw new IllegalArgumentException("account is empty");
		}
		Uri uri = Uri.parse(AUTHORITY_PLUS_CONVERSATIONS + account + "/"
				+ conversationId + "/messages");
		Cursor cursor = mContentResolver.query(uri, MESSAGE_PROJECTION, null,
				null, null);
		return new MessageCursor(this, mContentResolver, account, cursor);
	}

	/**
	 * Expunge the indicated message. One use of this is to discard drafts.
	 * 
	 * @param account
	 *            the account of the message id
	 * @param messageId
	 *            the id of the message to expunge
	 */
	public void expungeMessage(String account, long messageId) {
		if (TextUtils.isEmpty(account)) {
			throw new IllegalArgumentException("account is empty");
		}
		Uri uri = Uri
				.parse(AUTHORITY_PLUS_MESSAGES + account + "/" + messageId);
		mContentResolver.delete(uri, null, null);
	}

	/**
	 * Adds or removes the label on the conversation.
	 * 
	 * @param account
	 *            the account of the conversation
	 * @param conversationId
	 *            the conversation
	 * @param maxServerMessageId
	 *            the highest message id to whose labels should be changed. Note
	 *            that everywhere else in this file messageId means local
	 *            message id but here you need to use a server message id.
	 * @param label
	 *            the label to add or remove
	 * @param add
	 *            true to add the label, false to remove it
	 */
	public void addOrRemoveLabelOnConversation(String account,
			long conversationId, long maxServerMessageId, String label,
			boolean add) {
		if (TextUtils.isEmpty(account)) {
			throw new IllegalArgumentException("account is empty");
		}
		if (add) {
			Uri uri = Uri.parse(AUTHORITY_PLUS_CONVERSATIONS + account + "/"
					+ conversationId + "/labels");
			ContentValues values = new ContentValues();
			values.put(LabelColumns.CANONICAL_NAME, label);
			values.put(ConversationColumns.MAX_MESSAGE_ID, maxServerMessageId);
			mContentResolver.insert(uri, values);
		} else {
			String encodedLabel;
			try {
				encodedLabel = URLEncoder.encode(label, "utf-8");
			} catch (UnsupportedEncodingException e) {
				throw new RuntimeException(e);
			}
			Uri uri = Uri.parse(AUTHORITY_PLUS_CONVERSATIONS + account + "/"
					+ conversationId + "/labels/" + encodedLabel);
			mContentResolver.delete(uri, ConversationColumns.MAX_MESSAGE_ID,
					new String[] { "" + maxServerMessageId });
		}
	}

	/**
	 * Adds or removes the label on the message.
	 * 
	 * @param contentResolver
	 *            the content resolver.
	 * @param account
	 *            the account of the message
	 * @param conversationId
	 *            the conversation containing the message
	 * @param messageId
	 *            the id of the message to whose labels should be changed
	 * @param label
	 *            the label to add or remove
	 * @param add
	 *            true to add the label, false to remove it
	 */
	public static void addOrRemoveLabelOnMessage(
			ContentResolver contentResolver, String account,
			long conversationId, long messageId, String label, boolean add) {

		// conversationId is unused but we want to start passing it whereever we
		// pass a message id.
		if (add) {
			Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/"
					+ messageId + "/labels");
			ContentValues values = new ContentValues();
			values.put(LabelColumns.CANONICAL_NAME, label);
			contentResolver.insert(uri, values);
		} else {
			String encodedLabel;
			try {
				encodedLabel = URLEncoder.encode(label, "utf-8");
			} catch (UnsupportedEncodingException e) {
				throw new RuntimeException(e);
			}
			Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/"
					+ messageId + "/labels/" + encodedLabel);
			contentResolver.delete(uri, null, null);
		}
	}

	/**
	 * The mail provider will send an intent when certain changes happen in
	 * certain labels. Currently those labels are inbox and voicemail.
	 * 
	 * <p>
	 * The intent will have the action ACTION_PROVIDER_CHANGED and the extras
	 * mentioned below. The data for the intent will be
	 * content://gmail-ls/unread/<name of label>.
	 * 
	 * <p>
	 * The goal is to support the following user experience:
	 * <ul>
	 * <li>When present the new mail indicator reports the number of unread
	 * conversations in the inbox (or some other label).</li>
	 * <li>When the user views the inbox the indicator is removed immediately.
	 * They do not have to read all of the conversations.</li>
	 * <li>If more mail arrives the indicator reappears and shows the total
	 * number of unread conversations in the inbox.</li>
	 * <li>If the user reads the new conversations on the web the indicator
	 * disappears on the phone since there is no unread mail in the inbox that
	 * the user hasn't seen.</li>
	 * <li>The phone should vibrate/etc when it transitions from having no
	 * unseen unread inbox mail to having some.</li>
	 */

	/** The account in which the change occurred. */
	static public final String PROVIDER_CHANGED_EXTRA_ACCOUNT = "account";

	/** The number of unread conversations matching the label. */
	static public final String PROVIDER_CHANGED_EXTRA_COUNT = "count";

	/** Whether to get the user's attention, perhaps by vibrating. */
	static public final String PROVIDER_CHANGED_EXTRA_GET_ATTENTION = "getAttention";

	/**
	 * A label that is attached to all of the conversations being notified
	 * about. This enables the receiver of a notification to get a list of
	 * matching conversations.
	 */
	static public final String PROVIDER_CHANGED_EXTRA_TAG_LABEL = "tagLabel";

	/**
	 * Settings for which conversations should be synced to the phone.
	 * Conversations are synced if any message matches any of the following
	 * criteria:
	 * 
	 * <ul>
	 * <li>the message has a label in the include set</li>
	 * <li>the message is no older than conversationAgeDays and has a label in
	 * the partial set.</li>
	 * <li>also, pending changes on the server: the message has no
	 * user-controllable labels.</li>
	 * </ul>
	 * 
	 * <p>
	 * A user-controllable label is a user-defined label or star, inbox, trash,
	 * spam, etc. LABEL_UNREAD is not considered user-controllable.
	 */
	public static class Settings {
		public long conversationAgeDays;
		public long maxAttachmentSizeMb;
		public String[] labelsIncluded;
		public String[] labelsPartial;
	}

	/**
	 * Returns the settings.
	 * 
	 * @param account
	 *            the account whose setting should be retrieved
	 */
	public Settings getSettings(String account) {
		if (TextUtils.isEmpty(account)) {
			throw new IllegalArgumentException("account is empty");
		}
		Settings settings = new Settings();
		Cursor cursor = mContentResolver.query(Uri.withAppendedPath(
				SETTINGS_URI, account), SETTINGS_PROJECTION, null, null, null);
		cursor.moveToNext();
		settings.labelsIncluded = TextUtils.split(cursor.getString(0),
				SPACE_SEPARATOR_PATTERN);
		settings.labelsPartial = TextUtils.split(cursor.getString(1),
				SPACE_SEPARATOR_PATTERN);
		settings.conversationAgeDays = Long.parseLong(cursor.getString(2));
		settings.maxAttachmentSizeMb = Long.parseLong(cursor.getString(3));
		cursor.close();
		return settings;
	}

	/**
	 * Sets the settings. A sync will be scheduled automatically.
	 */
	public void setSettings(String account, Settings settings) {
		if (TextUtils.isEmpty(account)) {
			throw new IllegalArgumentException("account is empty");
		}
		ContentValues values = new ContentValues();
		values.put(SettingsColumns.LABELS_INCLUDED, TextUtils.join(" ",
				settings.labelsIncluded));
		values.put(SettingsColumns.LABELS_PARTIAL, TextUtils.join(" ",
				settings.labelsPartial));
		values.put(SettingsColumns.CONVERSATION_AGE_DAYS,
				settings.conversationAgeDays);
		values.put(SettingsColumns.MAX_ATTACHMENET_SIZE_MB,
				settings.maxAttachmentSizeMb);
		mContentResolver.update(Uri.withAppendedPath(SETTINGS_URI, account),
				values, null, null);
	}

	/**
	 * Uses sender instructions to build a formatted string.
	 * 
	 * <p>
	 * Sender list instructions contain compact information about the sender
	 * list. Most work that can be done without knowing how much room will be
	 * availble for the sender list is done when creating the instructions.
	 * 
	 * <p>
	 * The instructions string consists of tokens separated by
	 * SENDER_LIST_SEPARATOR. Here are the tokens, one per line:
	 * <ul>
	 * <li><tt>n</tt></li>
	 * <li><em>int</em>, the number of non-draft messages in the conversation</li>
	 * <li><tt>d</tt</li>
	 * <li><em>int</em>, the number of drafts in the conversation</li>
	 * <li><tt>l</tt></li>
	 * <li><em>literal html to be included in the output</em></li>
	 * <li><tt>s</tt> indicates that the message is sending (in the outbox
	 * without errors)</li>
	 * <li><tt>f</tt> indicates that the message failed to send (in the outbox
	 * with errors)</li>
	 * <li><em>for each message</em>
	 * <ul>
	 * <li><em>int</em>, 0 for read, 1 for unread</li>
	 * <li><em>int</em>, the priority of the message. Zero is the most important
	 * </li>
	 * <li><em>text</em>, the sender text or blank for messages from 'me'</li>
	 * </ul>
	 * </li>
	 * <li><tt>e</tt> to indicate that one or more messages have been elided</li>
	 * 
	 * <p>
	 * The instructions indicate how many messages and drafts are in the
	 * conversation and then describe the most important messages in order,
	 * indicating the priority of each message and whether the message is
	 * unread.
	 * 
	 * @param instructions
	 *            instructions as described above
	 * @param sb
	 *            the SpannableStringBuilder to append to
	 * @param maxChars
	 *            the number of characters available to display the text
	 * @param unreadStyle
	 *            the CharacterStyle for unread messages, or null
	 * @param draftsStyle
	 *            the CharacterStyle for draft messages, or null
	 * @param sendingString
	 *            the string to use when there are messages scheduled to be sent
	 * @param sendFailedString
	 *            the string to use when there are messages that mailed to send
	 * @param meString
	 *            the string to use for messages sent by this user
	 * @param draftString
	 *            the string to use for "Draft"
	 * @param draftPluralString
	 *            the string to use for "Drafts"
	 */
	public static void getSenderSnippet(String instructions,
			SpannableStringBuilder sb, int maxChars,
			CharacterStyle unreadStyle, CharacterStyle draftsStyle,
			CharSequence meString, CharSequence draftString,
			CharSequence draftPluralString, CharSequence sendingString,
			CharSequence sendFailedString, boolean forceAllUnread,
			boolean forceAllRead) {
		assert !(forceAllUnread && forceAllRead);
		boolean unreadStatusIsForced = forceAllUnread || forceAllRead;
		boolean forcedUnreadStatus = forceAllUnread;

		// Measure each fragment. It's ok to iterate over the entire set of
		// fragments because it is
		// never a long list, even if there are many senders.
		final Map<Integer, Integer> priorityToLength = sPriorityToLength;
		priorityToLength.clear();

		int maxFoundPriority = Integer.MIN_VALUE;
		int numMessages = 0;
		int numDrafts = 0;
		CharSequence draftsFragment = "";
		CharSequence sendingFragment = "";
		CharSequence sendFailedFragment = "";

		sSenderListSplitter.setString(instructions);
		int numFragments = 0;
		String[] fragments = sSenderFragments;
		int currentSize = fragments.length;
		while (sSenderListSplitter.hasNext()) {
			fragments[numFragments++] = sSenderListSplitter.next();
			if (numFragments == currentSize) {
				sSenderFragments = new String[2 * currentSize];
				System
						.arraycopy(fragments, 0, sSenderFragments, 0,
								currentSize);
				currentSize *= 2;
				fragments = sSenderFragments;
			}
		}

		for (int i = 0; i < numFragments;) {
			String fragment0 = fragments[i++];
			if ("".equals(fragment0)) {
				// This should be the final fragment.
			} else if (Gmail.SENDER_LIST_TOKEN_ELIDED.equals(fragment0)) {
				// ignore
			} else if (Gmail.SENDER_LIST_TOKEN_NUM_MESSAGES.equals(fragment0)) {
				numMessages = Integer.valueOf(fragments[i++]);
			} else if (Gmail.SENDER_LIST_TOKEN_NUM_DRAFTS.equals(fragment0)) {
				String numDraftsString = fragments[i++];
				numDrafts = Integer.parseInt(numDraftsString);
				draftsFragment = numDrafts == 1 ? draftString
						: draftPluralString + " (" + numDraftsString + ")";
			} else if (Gmail.SENDER_LIST_TOKEN_LITERAL.equals(fragment0)) {
				sb.append(Html.fromHtml(fragments[i++]));
				return;
			} else if (Gmail.SENDER_LIST_TOKEN_SENDING.equals(fragment0)) {
				sendingFragment = sendingString;
			} else if (Gmail.SENDER_LIST_TOKEN_SEND_FAILED.equals(fragment0)) {
				sendFailedFragment = sendFailedString;
			} else {
				String priorityString = fragments[i++];
				CharSequence nameString = fragments[i++];
				if (nameString.length() == 0)
					nameString = meString;
				int priority = Integer.parseInt(priorityString);
				priorityToLength.put(priority, nameString.length());
				maxFoundPriority = Math.max(maxFoundPriority, priority);
			}
		}
		String numMessagesFragment = (numMessages != 0) ? " ("
				+ Integer.toString(numMessages + numDrafts) + ")" : "";

		// Don't allocate fixedFragment unless we need it
		SpannableStringBuilder fixedFragment = null;
		int fixedFragmentLength = 0;
		if (draftsFragment.length() != 0) {
			if (fixedFragment == null) {
				fixedFragment = new SpannableStringBuilder();
			}
			fixedFragment.append(draftsFragment);
			if (draftsStyle != null) {
				fixedFragment.setSpan(CharacterStyle.wrap(draftsStyle), 0,
						fixedFragment.length(),
						Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
			}
		}
		if (sendingFragment.length() != 0) {
			if (fixedFragment == null) {
				fixedFragment = new SpannableStringBuilder();
			}
			if (fixedFragment.length() != 0)
				fixedFragment.append(", ");
			fixedFragment.append(sendingFragment);
		}
		if (sendFailedFragment.length() != 0) {
			if (fixedFragment == null) {
				fixedFragment = new SpannableStringBuilder();
			}
			if (fixedFragment.length() != 0)
				fixedFragment.append(", ");
			fixedFragment.append(sendFailedFragment);
		}

		if (fixedFragment != null) {
			fixedFragmentLength = fixedFragment.length();
		}

		final boolean normalMessagesExist = numMessagesFragment.length() != 0
				|| maxFoundPriority != Integer.MIN_VALUE;
		String preFixedFragement = "";
		if (normalMessagesExist && fixedFragmentLength != 0) {
			preFixedFragement = ", ";
		}
		int maxPriorityToInclude = -1; // inclusive
		int numCharsUsed = numMessagesFragment.length()
				+ preFixedFragement.length() + fixedFragmentLength;
		int numSendersUsed = 0;
		while (maxPriorityToInclude < maxFoundPriority) {
			if (priorityToLength.containsKey(maxPriorityToInclude + 1)) {
				int length = numCharsUsed
						+ priorityToLength.get(maxPriorityToInclude + 1);
				if (numCharsUsed > 0)
					length += 2;
				// We must show at least two senders if they exist. If we don't
				// have space for both
				// then we will truncate names.
				if (length > maxChars && numSendersUsed >= 2) {
					break;
				}
				numCharsUsed = length;
				numSendersUsed++;
			}
			maxPriorityToInclude++;
		}

		int numCharsToRemovePerWord = 0;
		if (numCharsUsed > maxChars) {
			numCharsToRemovePerWord = (numCharsUsed - maxChars)
					/ numSendersUsed;
		}

		boolean elided = false;
		for (int i = 0; i < numFragments;) {
			String fragment0 = fragments[i++];
			if ("".equals(fragment0)) {
				// This should be the final fragment.
			} else if (SENDER_LIST_TOKEN_ELIDED.equals(fragment0)) {
				elided = true;
			} else if (SENDER_LIST_TOKEN_NUM_MESSAGES.equals(fragment0)) {
				i++;
			} else if (SENDER_LIST_TOKEN_NUM_DRAFTS.equals(fragment0)) {
				i++;
			} else if (SENDER_LIST_TOKEN_SENDING.equals(fragment0)) {
			} else if (SENDER_LIST_TOKEN_SEND_FAILED.equals(fragment0)) {
			} else {
				final String unreadString = fragment0;
				final String priorityString = fragments[i++];
				String nameString = fragments[i++];
				if (nameString.length() == 0)
					nameString = meString.toString();
				if (numCharsToRemovePerWord != 0) {
					nameString = nameString.substring(0, Math.max(nameString
							.length()
							- numCharsToRemovePerWord, 0));
				}
				final boolean unread = unreadStatusIsForced ? forcedUnreadStatus
						: Integer.parseInt(unreadString) != 0;
				final int priority = Integer.parseInt(priorityString);
				if (priority <= maxPriorityToInclude) {
					if (sb.length() != 0) {
						sb.append(elided ? " .. " : ", ");
					}
					elided = false;
					int pos = sb.length();
					sb.append(nameString);
					if (unread && unreadStyle != null) {
						sb.setSpan(CharacterStyle.wrap(unreadStyle), pos, sb
								.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
					}
				} else {
					elided = true;
				}
			}
		}
		sb.append(numMessagesFragment);
		if (fixedFragmentLength != 0) {
			sb.append(preFixedFragement);
			sb.append(fixedFragment);
		}
	}

	/**
	 * This is a cursor that only defines methods to move throught the results
	 * and register to hear about changes. All access to the data is left to
	 * subinterfaces.
	 */
	public static class MailCursor extends ContentObserver {

		// A list of observers of this cursor.
		private final Set<MailCursorObserver> mObservers;

		// Updated values are accumulated here before being written out if the
		// cursor is asked to persist the changes.
		private ContentValues mUpdateValues;

		protected Cursor mCursor;
		protected String mAccount;

		public Cursor getCursor() {
			return mCursor;
		}

		/**
		 * Constructs the MailCursor given a regular cursor, registering as a
		 * change observer of the cursor.
		 * 
		 * @param account
		 *            the account the cursor is associated with
		 * @param cursor
		 *            the underlying cursor
		 */
		protected MailCursor(String account, Cursor cursor) {
			super(new Handler());
			mObservers = new HashSet<MailCursorObserver>();
			mCursor = cursor;
			mAccount = account;
			if (mCursor != null)
				mCursor.registerContentObserver(this);
		}

		/**
		 * Gets the account associated with this cursor.
		 * 
		 * @return the account.
		 */
		public String getAccount() {
			return mAccount;
		}

		protected void checkThread() {
			// Turn this on when activity code no longer runs in the sync thread
			// after notifications of changes.
			// Thread currentThread = Thread.currentThread();
			// if (currentThread != mThread) {
			// throw new RuntimeException("Accessed from the wrong thread");
			// }
		}

		/**
		 * Lazily constructs a map of update values to apply to the database if
		 * requested. This map is cleared out when we move to a different item
		 * in the result set.
		 * 
		 * @return a map of values to be applied by an update.
		 */
		protected ContentValues getUpdateValues() {
			if (mUpdateValues == null) {
				mUpdateValues = new ContentValues();
			}
			return mUpdateValues;
		}

		/**
		 * Called whenever mCursor is changed to point to a different row.
		 * Subclasses should override this if they need to clear out state when
		 * this happens.
		 * 
		 * Subclasses must call the inherited version if they override this.
		 */
		protected void onCursorPositionChanged() {
			mUpdateValues = null;
		}

		// ********* MailCursor

		/**
		 * Returns the numbers of rows in the cursor.
		 * 
		 * @return the number of rows in the cursor.
		 */
		final public int count() {
			if (mCursor != null) {
				return mCursor.getCount();
			} else {
				return 0;
			}
		}

		/**
		 * @return the current position of this cursor, or -1 if this cursor has
		 *         not been initialized.
		 */
		final public int position() {
			if (mCursor != null) {
				return mCursor.getPosition();
			} else {
				return -1;
			}
		}

		/**
		 * Move the cursor to an absolute position. The valid range of vaues is
		 * -1 &lt;= position &lt;= count.
		 * 
		 * <p>
		 * This method will return true if the request destination was
		 * reachable, otherwise it returns false.
		 * 
		 * @param position
		 *            the zero-based position to move to.
		 * @return whether the requested move fully succeeded.
		 */
		final public boolean moveTo(int position) {
			checkCursor();
			checkThread();
			boolean moved = mCursor.moveToPosition(position);
			if (moved)
				onCursorPositionChanged();
			return moved;
		}

		/**
		 * Move the cursor to the next row.
		 * 
		 * <p>
		 * This method will return false if the cursor is already past the last
		 * entry in the result set.
		 * 
		 * @return whether the move succeeded.
		 */
		final public boolean next() {
			checkCursor();
			checkThread();
			boolean moved = mCursor.moveToNext();
			if (moved)
				onCursorPositionChanged();
			return moved;
		}

		/**
		 * Release all resources and locks associated with the cursor. The
		 * cursor will not be valid after this function is called.
		 */
		final public void release() {
			if (mCursor != null) {
				mCursor.unregisterContentObserver(this);
				mCursor.deactivate();
			}
		}

		final public void registerContentObserver(ContentObserver observer) {
			mCursor.registerContentObserver(observer);
		}

		final public void unregisterContentObserver(ContentObserver observer) {
			mCursor.unregisterContentObserver(observer);
		}

		final public void registerDataSetObserver(DataSetObserver observer) {
			mCursor.registerDataSetObserver(observer);
		}

		final public void unregisterDataSetObserver(DataSetObserver observer) {
			mCursor.unregisterDataSetObserver(observer);
		}

		/**
		 * Register an observer to hear about changes to the cursor.
		 * 
		 * @param observer
		 *            the observer to register
		 */
		final public void registerObserver(MailCursorObserver observer) {
			mObservers.add(observer);
		}

		/**
		 * Unregister an observer.
		 * 
		 * @param observer
		 *            the observer to unregister
		 */
		final public void unregisterObserver(MailCursorObserver observer) {
			mObservers.remove(observer);
		}

		// ********* ContentObserver

		@Override
		final public boolean deliverSelfNotifications() {
			return false;
		}

		@Override
		public void onChange(boolean selfChange) {
			if (DEBUG) {
				Log.d(TAG, "MailCursor is notifying " + mObservers.size()
						+ " observers");
			}
			for (MailCursorObserver o : mObservers) {
				o.onCursorChanged(this);
			}
		}

		protected void checkCursor() {
			if (mCursor == null) {
				throw new IllegalStateException(
						"cannot read from an insertion cursor");
			}
		}

		/**
		 * Returns the string value of the column, or "" if the value is null.
		 */
		protected String getStringInColumn(int columnIndex) {
			checkCursor();
			return toNonnullString(mCursor.getString(columnIndex));
		}
	}

	/**
	 * A MailCursor observer is notified of changes to the result set of a
	 * cursor.
	 */
	public interface MailCursorObserver {

		/**
		 * Called when the result set of a cursor has changed.
		 * 
		 * @param cursor
		 *            the cursor whose result set has changed.
		 */
		void onCursorChanged(MailCursor cursor);
	}

	/**
	 * A cursor over labels.
	 */
	public final class LabelCursor extends MailCursor {

		private final int mNameIndex;
		private final int mNumConversationsIndex;
		private final int mNumUnreadConversationsIndex;

		private LabelCursor(String account, Cursor cursor) {
			super(account, cursor);

			mNameIndex = mCursor
					.getColumnIndexOrThrow(LabelColumns.CANONICAL_NAME);
			mNumConversationsIndex = mCursor
					.getColumnIndexOrThrow(LabelColumns.NUM_CONVERSATIONS);
			mNumUnreadConversationsIndex = mCursor
					.getColumnIndexOrThrow(LabelColumns.NUM_UNREAD_CONVERSATIONS);
		}

		/**
		 * Gets the canonical name of the current label.
		 * 
		 * @return the current label's name.
		 */
		public String getName() {
			return getStringInColumn(mNameIndex);
		}

		/**
		 * Gets the number of conversations with this label.
		 * 
		 * @return the number of conversations with this label.
		 */
		public int getNumConversations() {
			return mCursor.getInt(mNumConversationsIndex);
		}

		/**
		 * Gets the number of unread conversations with this label.
		 * 
		 * @return the number of unread conversations with this label.
		 */
		public int getNumUnreadConversations() {
			return mCursor.getInt(mNumUnreadConversationsIndex);
		}
	}

	/**
	 * This is a map of labels. TODO: make it observable.
	 */
	public static final class LabelMap extends Observable {
		private final static ContentValues EMPTY_CONTENT_VALUES = new ContentValues();

		private ContentQueryMap mQueryMap;
		private SortedSet<String> mSortedUserLabels;
		private Map<String, Long> mCanonicalNameToId;

		private long mLabelIdSent;
		private long mLabelIdInbox;
		private long mLabelIdDraft;
		private long mLabelIdUnread;
		private long mLabelIdTrash;
		private long mLabelIdSpam;
		private long mLabelIdStarred;
		private long mLabelIdChat;
		private long mLabelIdVoicemail;
		private long mLabelIdIgnored;
		private long mLabelIdVoicemailInbox;
		private long mLabelIdCached;
		private long mLabelIdOutbox;

		private boolean mLabelsSynced = false;

		public LabelMap(ContentResolver contentResolver, String account,
				boolean keepUpdated) {
			if (TextUtils.isEmpty(account)) {
				throw new IllegalArgumentException("account is empty");
			}
			Cursor cursor = contentResolver.query(Uri.withAppendedPath(
					LABELS_URI, account), LABEL_PROJECTION, null, null, null);
			init(cursor, keepUpdated);
		}

		public LabelMap(Cursor cursor, boolean keepUpdated) {
			init(cursor, keepUpdated);
		}

		private void init(Cursor cursor, boolean keepUpdated) {
			mQueryMap = new ContentQueryMap(cursor, BaseColumns._ID,
					keepUpdated, null);
			mSortedUserLabels = new TreeSet<String>(java.text.Collator
					.getInstance());
			mCanonicalNameToId = Maps.newHashMap();
			updateDataStructures();
			mQueryMap.addObserver(new Observer() {
				public void update(Observable observable, Object data) {
					updateDataStructures();
					setChanged();
					notifyObservers();
				}
			});
		}

		/**
		 * @return whether at least some labels have been synced.
		 */
		public boolean labelsSynced() {
			return mLabelsSynced;
		}

		/**
		 * Updates the data structures that are maintained separately from
		 * mQueryMap after the query map has changed.
		 */
		private void updateDataStructures() {
			mSortedUserLabels.clear();
			mCanonicalNameToId.clear();
			for (Map.Entry<String, ContentValues> row : mQueryMap.getRows()
					.entrySet()) {
				long labelId = Long.valueOf(row.getKey());
				String canonicalName = row.getValue().getAsString(
						LabelColumns.CANONICAL_NAME);
				if (isLabelUserDefined(canonicalName)) {
					mSortedUserLabels.add(canonicalName);
				}
				mCanonicalNameToId.put(canonicalName, labelId);

				if (LABEL_SENT.equals(canonicalName)) {
					mLabelIdSent = labelId;
				} else if (LABEL_INBOX.equals(canonicalName)) {
					mLabelIdInbox = labelId;
				} else if (LABEL_DRAFT.equals(canonicalName)) {
					mLabelIdDraft = labelId;
				} else if (LABEL_UNREAD.equals(canonicalName)) {
					mLabelIdUnread = labelId;
				} else if (LABEL_TRASH.equals(canonicalName)) {
					mLabelIdTrash = labelId;
				} else if (LABEL_SPAM.equals(canonicalName)) {
					mLabelIdSpam = labelId;
				} else if (LABEL_STARRED.equals(canonicalName)) {
					mLabelIdStarred = labelId;
				} else if (LABEL_CHAT.equals(canonicalName)) {
					mLabelIdChat = labelId;
				} else if (LABEL_IGNORED.equals(canonicalName)) {
					mLabelIdIgnored = labelId;
				} else if (LABEL_VOICEMAIL.equals(canonicalName)) {
					mLabelIdVoicemail = labelId;
				} else if (LABEL_VOICEMAIL_INBOX.equals(canonicalName)) {
					mLabelIdVoicemailInbox = labelId;
				} else if (LABEL_CACHED.equals(canonicalName)) {
					mLabelIdCached = labelId;
				} else if (LABEL_OUTBOX.equals(canonicalName)) {
					mLabelIdOutbox = labelId;
				}
				mLabelsSynced = mLabelIdSent != 0 && mLabelIdInbox != 0
						&& mLabelIdDraft != 0 && mLabelIdUnread != 0
						&& mLabelIdTrash != 0 && mLabelIdSpam != 0
						&& mLabelIdStarred != 0 && mLabelIdChat != 0
						&& mLabelIdIgnored != 0 && mLabelIdVoicemail != 0;
			}
		}

		public long getLabelIdSent() {
			checkLabelsSynced();
			return mLabelIdSent;
		}

		public long getLabelIdInbox() {
			checkLabelsSynced();
			return mLabelIdInbox;
		}

		public long getLabelIdDraft() {
			checkLabelsSynced();
			return mLabelIdDraft;
		}

		public long getLabelIdUnread() {
			checkLabelsSynced();
			return mLabelIdUnread;
		}

		public long getLabelIdTrash() {
			checkLabelsSynced();
			return mLabelIdTrash;
		}

		public long getLabelIdSpam() {
			checkLabelsSynced();
			return mLabelIdSpam;
		}

		public long getLabelIdStarred() {
			checkLabelsSynced();
			return mLabelIdStarred;
		}

		public long getLabelIdChat() {
			checkLabelsSynced();
			return mLabelIdChat;
		}

		public long getLabelIdIgnored() {
			checkLabelsSynced();
			return mLabelIdIgnored;
		}

		public long getLabelIdVoicemail() {
			checkLabelsSynced();
			return mLabelIdVoicemail;
		}

		public long getLabelIdVoicemailInbox() {
			checkLabelsSynced();
			return mLabelIdVoicemailInbox;
		}

		public long getLabelIdCached() {
			checkLabelsSynced();
			return mLabelIdCached;
		}

		public long getLabelIdOutbox() {
			checkLabelsSynced();
			return mLabelIdOutbox;
		}

		private void checkLabelsSynced() {
			if (!labelsSynced()) {
				throw new IllegalStateException("LabelMap not initalized");
			}
		}

		/** Returns the list of user-defined labels in alphabetical order. */
		public SortedSet<String> getSortedUserLabels() {
			return mSortedUserLabels;
		}

		private static final List<String> SORTED_USER_MEANINGFUL_SYSTEM_LABELS = Lists
				.newArrayList(LABEL_INBOX, LABEL_STARRED, LABEL_CHAT,
						LABEL_SENT, LABEL_OUTBOX, LABEL_DRAFT, LABEL_ALL,
						LABEL_SPAM, LABEL_TRASH);

		private static final Set<String> USER_MEANINGFUL_SYSTEM_LABELS_SET = Sets
				.newHashSet(SORTED_USER_MEANINGFUL_SYSTEM_LABELS
						.toArray(new String[] {}));

		public static List<String> getSortedUserMeaningfulSystemLabels() {
			return SORTED_USER_MEANINGFUL_SYSTEM_LABELS;
		}

		public static Set<String> getUserMeaningfulSystemLabelsSet() {
			return USER_MEANINGFUL_SYSTEM_LABELS_SET;
		}

		/**
		 * If you are ever tempted to remove outbox or draft from this set make
		 * sure you have a way to stop draft and outbox messages from getting
		 * purged before they are sent to the server.
		 */
		private static final Set<String> FORCED_INCLUDED_LABELS = Sets
				.newHashSet(LABEL_OUTBOX, LABEL_DRAFT);

		public static Set<String> getForcedIncludedLabels() {
			return FORCED_INCLUDED_LABELS;
		}

		private static final Set<String> FORCED_INCLUDED_OR_PARTIAL_LABELS = Sets
				.newHashSet(LABEL_INBOX);

		public static Set<String> getForcedIncludedOrPartialLabels() {
			return FORCED_INCLUDED_OR_PARTIAL_LABELS;
		}

		private static final Set<String> FORCED_UNSYNCED_LABELS = Sets
				.newHashSet(LABEL_ALL, LABEL_CHAT, LABEL_SPAM, LABEL_TRASH);

		public static Set<String> getForcedUnsyncedLabels() {
			return FORCED_UNSYNCED_LABELS;
		}

		/**
		 * Returns the number of conversation with a given label.
		 * 
		 * @deprecated Use {@link #getLabelId} instead.
		 */
		@Deprecated
		public int getNumConversations(String label) {
			return getNumConversations(getLabelId(label));
		}

		/** Returns the number of conversation with a given label. */
		public int getNumConversations(long labelId) {
			return getLabelIdValues(labelId).getAsInteger(
					LabelColumns.NUM_CONVERSATIONS);
		}

		/**
		 * Returns the number of unread conversation with a given label.
		 * 
		 * @deprecated Use {@link #getLabelId} instead.
		 */
		@Deprecated
		public int getNumUnreadConversations(String label) {
			return getNumUnreadConversations(getLabelId(label));
		}

		/** Returns the number of unread conversation with a given label. */
		public int getNumUnreadConversations(long labelId) {
			Integer unreadConversations = getLabelIdValues(labelId)
					.getAsInteger(LabelColumns.NUM_UNREAD_CONVERSATIONS);
			// There seems to be a race condition here that can get the label
			// maps into a bad
			// state and lose state on a particular label.
			int result = 0;
			if (unreadConversations != null) {
				result = unreadConversations < 0 ? 0 : unreadConversations;
			}

			return result;
		}

		/**
		 * @return the canonical name for a label
		 */
		public String getCanonicalName(long labelId) {
			return getLabelIdValues(labelId).getAsString(
					LabelColumns.CANONICAL_NAME);
		}

		/**
		 * @return the human name for a label
		 */
		public String getName(long labelId) {
			return getLabelIdValues(labelId).getAsString(LabelColumns.NAME);
		}

		/**
		 * @return whether a given label is known
		 */
		public boolean hasLabel(long labelId) {
			return mQueryMap.getRows().containsKey(Long.toString(labelId));
		}

		/**
		 * @return returns the id of a label given the canonical name
		 * @deprecated this is only needed because most of the UI uses label
		 *             names instead of ids
		 */
		@Deprecated
		public long getLabelId(String canonicalName) {
			if (mCanonicalNameToId.containsKey(canonicalName)) {
				return mCanonicalNameToId.get(canonicalName);
			} else {
				throw new IllegalArgumentException("Unknown canonical name: "
						+ canonicalName);
			}
		}

		private ContentValues getLabelIdValues(long labelId) {
			final ContentValues values = mQueryMap.getValues(Long
					.toString(labelId));
			if (values != null) {
				return values;
			} else {
				return EMPTY_CONTENT_VALUES;
			}
		}

		/**
		 * Force the map to requery. This should not be necessary outside tests.
		 */
		public void requery() {
			mQueryMap.requery();
		}

		public void close() {
			mQueryMap.close();
		}
	}

	private final Map<String, Gmail.LabelMap> mLabelMaps = Maps.newHashMap();

	public LabelMap getLabelMap(String account) {
		Gmail.LabelMap labelMap = mLabelMaps.get(account);
		if (labelMap == null) {
			labelMap = new Gmail.LabelMap(mContentResolver, account, true /* keepUpdated */);
			mLabelMaps.put(account, labelMap);
		}
		return labelMap;
	}

	public enum PersonalLevel {
		NOT_TO_ME(0), TO_ME_AND_OTHERS(1), ONLY_TO_ME(2);

		private int mLevel;

		PersonalLevel(int level) {
			mLevel = level;
		}

		public int toInt() {
			return mLevel;
		}

		public static PersonalLevel fromInt(int level) {
			switch (level) {
			case 0:
				return NOT_TO_ME;
			case 1:
				return TO_ME_AND_OTHERS;
			case 2:
				return ONLY_TO_ME;
			default:
				throw new IllegalArgumentException(level
						+ " is not a personal level");
			}
		}
	}

	/**
	 * Indicates a version of an attachment.
	 */
	public enum AttachmentRendition {
		/**
		 * The full version of an attachment if it can be handled on the device,
		 * otherwise the preview.
		 */
		BEST,

		/**
		 * A smaller or simpler version of the attachment, such as a scaled-down
		 * image or an HTML version of a document. Not always available.
		 */
		SIMPLE,
	}

	/**
	 * The columns that can be requested when querying an attachment's download
	 * URI. See getAttachmentDownloadUri.
	 */
	public static final class AttachmentColumns implements BaseColumns {

		/** Contains a STATUS value from {@link android.provider.Downloads} */
		public static final String STATUS = "status";

		/**
		 * The name of the file to open (with ContentProvider.open). If this is
		 * empty then continue to use the attachment's URI.
		 * 
		 * TODO: I'm not sure that we need this. See the note in CL 66853-p9.
		 */
		public static final String FILENAME = "filename";
	}

	/**
	 * We track where an attachment came from so that we know how to download it
	 * and include it in new messages.
	 */
	public enum AttachmentOrigin {
		/** Extras are "<conversationId>-<messageId>-<partId>". */
		SERVER_ATTACHMENT,
		/** Extras are "<path>". */
		LOCAL_FILE;

		private static final String SERVER_EXTRAS_SEPARATOR = "_";

		public static String serverExtras(long conversationId, long messageId,
				String partId) {
			return conversationId + SERVER_EXTRAS_SEPARATOR + messageId
					+ SERVER_EXTRAS_SEPARATOR + partId;
		}

		/**
		 * @param extras
		 *            extras as returned by serverExtras
		 * @return an array of conversationId, messageId, partId (all as
		 *         strings)
		 */
		public static String[] splitServerExtras(String extras) {
			return TextUtils.split(extras, SERVER_EXTRAS_SEPARATOR);
		}

		public static String localFileExtras(Uri path) {
			return path.toString();
		}
	}

	public static final class Attachment {
		/** Identifies the attachment uniquely when combined wih a message id. */
		public String partId;

		/** The intended filename of the attachment. */
		public String name;

		/** The native content type. */
		public String contentType;

		/** The size of the attachment in its native form. */
		public int size;

		/**
		 * The content type of the simple version of the attachment. Blank if no
		 * simple version is available.
		 */
		public String simpleContentType;

		public AttachmentOrigin origin;

		public String originExtras;

		public String toJoinedString() {
			return TextUtils.join("|", Lists.newArrayList(partId == null ? ""
					: partId, name.replace("|", ""), contentType, size,
					simpleContentType, origin.toString(), originExtras));
		}

		public static Attachment parseJoinedString(String joinedString) {
			String[] fragments = TextUtils.split(joinedString, "\\|");
			int i = 0;
			Attachment attachment = new Attachment();
			attachment.partId = fragments[i++];
			if (TextUtils.isEmpty(attachment.partId)) {
				attachment.partId = null;
			}
			attachment.name = fragments[i++];
			attachment.contentType = fragments[i++];
			attachment.size = Integer.parseInt(fragments[i++]);
			attachment.simpleContentType = fragments[i++];
			attachment.origin = AttachmentOrigin.valueOf(fragments[i++]);
			attachment.originExtras = fragments[i++];
			return attachment;
		}
	}

	/**
	 * Any given attachment can come in two different renditions (see
	 * {@link android.provider.Gmail.AttachmentRendition}) and can be saved to
	 * the sd card or to a cache. The gmail provider automatically syncs some
	 * attachments to the cache. Other attachments can be downloaded on demand.
	 * Attachments in the cache will be purged as needed to save space.
	 * Attachments on the SD card must be managed by the user or other software.
	 * 
	 * @param account
	 *            which account to use
	 * @param messageId
	 *            the id of the mesage with the attachment
	 * @param attachment
	 *            the attachment
	 * @param rendition
	 *            the desired rendition
	 * @param saveToSd
	 *            whether the attachment should be saved to (or loaded from) the
	 *            sd card or
	 * @return the URI to ask the content provider to open in order to open an
	 *         attachment.
	 */
	public static Uri getAttachmentUri(String account, long messageId,
			Attachment attachment, AttachmentRendition rendition,
			boolean saveToSd) {
		if (TextUtils.isEmpty(account)) {
			throw new IllegalArgumentException("account is empty");
		}
		if (attachment.origin == AttachmentOrigin.LOCAL_FILE) {
			return Uri.parse(attachment.originExtras);
		} else {
			return Uri.parse(AUTHORITY_PLUS_MESSAGES).buildUpon().appendPath(
					account).appendPath(Long.toString(messageId)).appendPath(
					"attachments").appendPath(attachment.partId).appendPath(
					rendition.toString())
					.appendPath(Boolean.toString(saveToSd)).build();
		}
	}

	/**
	 * Return the URI to query in order to find out whether an attachment is
	 * downloaded.
	 * 
	 * <p>
	 * Querying this will also start a download if necessary. The cursor
	 * returned by querying this URI can contain the columns in
	 * {@link android.provider.Gmail.AttachmentColumns}.
	 * 
	 * <p>
	 * Deleting this URI will cancel the download if it was not started
	 * automatically by the provider. It will also remove bookkeeping for
	 * saveToSd downloads.
	 * 
	 * @param attachmentUri
	 *            the attachment URI as returned by getAttachmentUri. The URI's
	 *            authority Gmail.AUTHORITY. If it is not then you should open
	 *            the file directly.
	 */
	public static Uri getAttachmentDownloadUri(Uri attachmentUri) {
		if (!"content".equals(attachmentUri.getScheme())) {
			throw new IllegalArgumentException(
					"Uri's scheme must be 'content': " + attachmentUri);
		}
		return attachmentUri.buildUpon().appendPath("download").build();
	}

	public enum CursorStatus {
		LOADED, LOADING, ERROR, // A network error occurred.
	}

	/**
	 * A cursor over messages.
	 */
	public static final class MessageCursor extends MailCursor {

		private LabelMap mLabelMap;

		private final ContentResolver mContentResolver;

		/**
		 * Only valid if mCursor == null, in which case we are inserting a new
		 * message.
		 */
		long mInReplyToLocalMessageId;
		boolean mPreserveAttachments;

		private int mIdIndex;
		private int mConversationIdIndex;
		private int mSubjectIndex;
		private int mSnippetIndex;
		private int mFromIndex;
		private int mToIndex;
		private int mCcIndex;
		private int mBccIndex;
		private int mReplyToIndex;
		private int mDateSentMsIndex;
		private int mDateReceivedMsIndex;
		private int mListInfoIndex;
		private int mPersonalLevelIndex;
		private int mBodyIndex;
		private int mBodyEmbedsExternalResourcesIndex;
		private int mLabelIdsIndex;
		private int mJoinedAttachmentInfosIndex;
		private int mErrorIndex;

		private final TextUtils.StringSplitter mLabelIdsSplitter = newMessageLabelIdsSplitter();

		public MessageCursor(Gmail gmail, ContentResolver cr, String account,
				Cursor cursor) {
			super(account, cursor);
			mLabelMap = gmail.getLabelMap(account);
			if (cursor == null) {
				throw new IllegalArgumentException(
						"null cursor passed to MessageCursor()");
			}

			mContentResolver = cr;

			mIdIndex = mCursor.getColumnIndexOrThrow(MessageColumns.ID);
			mConversationIdIndex = mCursor
					.getColumnIndexOrThrow(MessageColumns.CONVERSATION_ID);
			mSubjectIndex = mCursor
					.getColumnIndexOrThrow(MessageColumns.SUBJECT);
			mSnippetIndex = mCursor
					.getColumnIndexOrThrow(MessageColumns.SNIPPET);
			mFromIndex = mCursor.getColumnIndexOrThrow(MessageColumns.FROM);
			mToIndex = mCursor.getColumnIndexOrThrow(MessageColumns.TO);
			mCcIndex = mCursor.getColumnIndexOrThrow(MessageColumns.CC);
			mBccIndex = mCursor.getColumnIndexOrThrow(MessageColumns.BCC);
			mReplyToIndex = mCursor
					.getColumnIndexOrThrow(MessageColumns.REPLY_TO);
			mDateSentMsIndex = mCursor
					.getColumnIndexOrThrow(MessageColumns.DATE_SENT_MS);
			mDateReceivedMsIndex = mCursor
					.getColumnIndexOrThrow(MessageColumns.DATE_RECEIVED_MS);
			mListInfoIndex = mCursor
					.getColumnIndexOrThrow(MessageColumns.LIST_INFO);
			mPersonalLevelIndex = mCursor
					.getColumnIndexOrThrow(MessageColumns.PERSONAL_LEVEL);
			mBodyIndex = mCursor.getColumnIndexOrThrow(MessageColumns.BODY);
			mBodyEmbedsExternalResourcesIndex = mCursor
					.getColumnIndexOrThrow(MessageColumns.EMBEDS_EXTERNAL_RESOURCES);
			mLabelIdsIndex = mCursor
					.getColumnIndexOrThrow(MessageColumns.LABEL_IDS);
			mJoinedAttachmentInfosIndex = mCursor
					.getColumnIndexOrThrow(MessageColumns.JOINED_ATTACHMENT_INFOS);
			mErrorIndex = mCursor.getColumnIndexOrThrow(MessageColumns.ERROR);

			mInReplyToLocalMessageId = 0;
			mPreserveAttachments = false;
		}

		protected MessageCursor(ContentResolver cr, String account,
				long inReplyToMessageId, boolean preserveAttachments) {
			super(account, null);
			mContentResolver = cr;
			mInReplyToLocalMessageId = inReplyToMessageId;
			mPreserveAttachments = preserveAttachments;
		}

		@Override
		protected void onCursorPositionChanged() {
			super.onCursorPositionChanged();
		}

		public CursorStatus getStatus() {
			Bundle extras = mCursor.getExtras();
			String stringStatus = extras.getString(EXTRA_STATUS);
			return CursorStatus.valueOf(stringStatus);
		}

		/** Retry a network request after errors. */
		public void retry() {
			Bundle input = new Bundle();
			input.putString(RESPOND_INPUT_COMMAND, COMMAND_RETRY);
			Bundle output = mCursor.respond(input);
			String response = output.getString(RESPOND_OUTPUT_COMMAND_RESPONSE);
			assert COMMAND_RESPONSE_OK.equals(response);
		}

		/**
		 * Gets the message id of the current message. Note that this is an
		 * immutable local message (not, for example, GMail's message id, which
		 * is immutable).
		 * 
		 * @return the message's id
		 */
		public long getMessageId() {
			checkCursor();
			return mCursor.getLong(mIdIndex);
		}

		/**
		 * Gets the message's conversation id. This must be immutable. (For
		 * example, with GMail this should be the original conversation id
		 * rather than the default notion of converation id.)
		 * 
		 * @return the message's conversation id
		 */
		public long getConversationId() {
			checkCursor();
			return mCursor.getLong(mConversationIdIndex);
		}

		/**
		 * Gets the message's subject.
		 * 
		 * @return the message's subject
		 */
		public String getSubject() {
			return getStringInColumn(mSubjectIndex);
		}

		/**
		 * Gets the message's snippet (the short piece of the body). The snippet
		 * is generated from the body and cannot be set directly.
		 * 
		 * @return the message's snippet
		 */
		public String getSnippet() {
			return getStringInColumn(mSnippetIndex);
		}

		/**
		 * Gets the message's from address.
		 * 
		 * @return the message's from address
		 */
		public String getFromAddress() {
			return getStringInColumn(mFromIndex);
		}

		/**
		 * Returns the addresses for the key, if it has been updated, or index
		 * otherwise.
		 */
		private String[] getAddresses(String key, int index) {
			ContentValues updated = getUpdateValues();
			String addresses;
			if (updated.containsKey(key)) {
				addresses = (String) getUpdateValues().get(key);
			} else {
				addresses = getStringInColumn(index);
			}

			return TextUtils.split(addresses, EMAIL_SEPARATOR_PATTERN);
		}

		/**
		 * Gets the message's to addresses.
		 * 
		 * @return the message's to addresses
		 */
		public String[] getToAddresses() {
			return getAddresses(MessageColumns.TO, mToIndex);
		}

		/**
		 * Gets the message's cc addresses.
		 * 
		 * @return the message's cc addresses
		 */
		public String[] getCcAddresses() {
			return getAddresses(MessageColumns.CC, mCcIndex);
		}

		/**
		 * Gets the message's bcc addresses.
		 * 
		 * @return the message's bcc addresses
		 */
		public String[] getBccAddresses() {
			return getAddresses(MessageColumns.BCC, mBccIndex);
		}

		/**
		 * Gets the message's replyTo address.
		 * 
		 * @return the message's replyTo address
		 */
		public String[] getReplyToAddress() {
			return TextUtils.split(getStringInColumn(mReplyToIndex),
					EMAIL_SEPARATOR_PATTERN);
		}

		public long getDateSentMs() {
			checkCursor();
			return mCursor.getLong(mDateSentMsIndex);
		}

		public long getDateReceivedMs() {
			checkCursor();
			return mCursor.getLong(mDateReceivedMsIndex);
		}

		public String getListInfo() {
			return getStringInColumn(mListInfoIndex);
		}

		public PersonalLevel getPersonalLevel() {
			checkCursor();
			int personalLevelInt = mCursor.getInt(mPersonalLevelIndex);
			return PersonalLevel.fromInt(personalLevelInt);
		}

		/**
		 * @deprecated Always returns true.
		 */
		@Deprecated
		public boolean getExpanded() {
			return true;
		}

		/**
		 * Gets the message's body.
		 * 
		 * @return the message's body
		 */
		public String getBody() {
			return getStringInColumn(mBodyIndex);
		}

		/**
		 * @return whether the message's body contains embedded references to
		 *         external resources. In that case the resources should only be
		 *         displayed if the user explicitly asks for them to be
		 */
		public boolean getBodyEmbedsExternalResources() {
			checkCursor();
			return mCursor.getInt(mBodyEmbedsExternalResourcesIndex) != 0;
		}

		/**
		 * @return a copy of the set of label ids
		 */
		public Set<Long> getLabelIds() {
			String labelNames = mCursor.getString(mLabelIdsIndex);
			mLabelIdsSplitter.setString(labelNames);
			return getLabelIdsFromLabelIdsString(mLabelIdsSplitter);
		}

		/**
		 * @return a joined string of labels separated by spaces.
		 */
		public String getRawLabelIds() {
			return mCursor.getString(mLabelIdsIndex);
		}

		/**
		 * Adds a label to a message (if add is true) or removes it (if add is
		 * false).
		 * 
		 * @param label
		 *            the label to add or remove
		 * @param add
		 *            whether to add or remove the label
		 */
		public void addOrRemoveLabel(String label, boolean add) {
			addOrRemoveLabelOnMessage(mContentResolver, mAccount,
					getConversationId(), getMessageId(), label, add);
		}

		public ArrayList<Attachment> getAttachmentInfos() {
			ArrayList<Attachment> attachments = Lists.newArrayList();

			String joinedAttachmentInfos = mCursor
					.getString(mJoinedAttachmentInfosIndex);
			if (joinedAttachmentInfos != null) {
				for (String joinedAttachmentInfo : TextUtils.split(
						joinedAttachmentInfos,
						ATTACHMENT_INFO_SEPARATOR_PATTERN)) {

					Attachment attachment = Attachment
							.parseJoinedString(joinedAttachmentInfo);
					attachments.add(attachment);
				}
			}
			return attachments;
		}

		/**
		 * @return the error text for the message. Error text gets set if the
		 *         server rejects a message that we try to save or send. If
		 *         there is error text then the message is no longer scheduled
		 *         to be saved or sent. Calling save() or send() will clear any
		 *         error as well as scheduling another atempt to save or send
		 *         the message.
		 */
		public String getErrorText() {
			return mCursor.getString(mErrorIndex);
		}
	}

	/**
	 * A helper class for creating or updating messags. Use the putXxx methods
	 * to provide initial or new values for the message. Then save or send the
	 * message. To save or send an existing message without making other changes
	 * to it simply provide an emty ContentValues.
	 */
	public static class MessageModification {

		/**
		 * Sets the message's subject. Only valid for drafts.
		 * 
		 * @param values
		 *            the ContentValues that will be used to create or update
		 *            the message
		 * @param subject
		 *            the new subject
		 */
		public static void putSubject(ContentValues values, String subject) {
			values.put(MessageColumns.SUBJECT, subject);
		}

		/**
		 * Sets the message's to address. Only valid for drafts.
		 * 
		 * @param values
		 *            the ContentValues that will be used to create or update
		 *            the message
		 * @param toAddresses
		 *            the new to addresses
		 */
		public static void putToAddresses(ContentValues values,
				String[] toAddresses) {
			values.put(MessageColumns.TO, TextUtils.join(EMAIL_SEPARATOR,
					toAddresses));
		}

		/**
		 * Sets the message's cc address. Only valid for drafts.
		 * 
		 * @param values
		 *            the ContentValues that will be used to create or update
		 *            the message
		 * @param ccAddresses
		 *            the new cc addresses
		 */
		public static void putCcAddresses(ContentValues values,
				String[] ccAddresses) {
			values.put(MessageColumns.CC, TextUtils.join(EMAIL_SEPARATOR,
					ccAddresses));
		}

		/**
		 * Sets the message's bcc address. Only valid for drafts.
		 * 
		 * @param values
		 *            the ContentValues that will be used to create or update
		 *            the message
		 * @param bccAddresses
		 *            the new bcc addresses
		 */
		public static void putBccAddresses(ContentValues values,
				String[] bccAddresses) {
			values.put(MessageColumns.BCC, TextUtils.join(EMAIL_SEPARATOR,
					bccAddresses));
		}

		/**
		 * Saves a new body for the message. Only valid for drafts.
		 * 
		 * @param values
		 *            the ContentValues that will be used to create or update
		 *            the message
		 * @param body
		 *            the new body of the message
		 */
		public static void putBody(ContentValues values, String body) {
			values.put(MessageColumns.BODY, body);
		}

		/**
		 * Sets the attachments on a message. Only valid for drafts.
		 * 
		 * @param values
		 *            the ContentValues that will be used to create or update
		 *            the message
		 * @param attachments
		 */
		public static void putAttachments(ContentValues values,
				List<Attachment> attachments) {
			values.put(MessageColumns.JOINED_ATTACHMENT_INFOS,
					joinedAttachmentsString(attachments));
		}

		/**
		 * Create a new message and save it as a draft or send it.
		 * 
		 * @param contentResolver
		 *            the content resolver to use
		 * @param account
		 *            the account to use
		 * @param values
		 *            the values for the new message
		 * @param refMessageId
		 *            the message that is being replied to or forwarded
		 * @param save
		 *            whether to save or send the message
		 * @return the id of the new message
		 */
		public static long sendOrSaveNewMessage(
				ContentResolver contentResolver, String account,
				ContentValues values, long refMessageId, boolean save) {
			values.put(MessageColumns.FAKE_SAVE, save);
			values.put(MessageColumns.FAKE_REF_MESSAGE_ID, refMessageId);
			Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/");
			Uri result = contentResolver.insert(uri, values);
			return ContentUris.parseId(result);
		}

		/**
		 * Update an existing draft and save it as a new draft or send it.
		 * 
		 * @param contentResolver
		 *            the content resolver to use
		 * @param account
		 *            the account to use
		 * @param messageId
		 *            the id of the message to update
		 * @param updateValues
		 *            the values to change. Unspecified fields will not be
		 *            altered
		 * @param save
		 *            whether to resave the message as a draft or send it
		 */
		public static void sendOrSaveExistingMessage(
				ContentResolver contentResolver, String account,
				long messageId, ContentValues updateValues, boolean save) {
			updateValues.put(MessageColumns.FAKE_SAVE, save);
			updateValues.put(MessageColumns.FAKE_REF_MESSAGE_ID, 0);
			Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/"
					+ messageId);
			contentResolver.update(uri, updateValues, null, null);
		}

		/**
		 * The string produced here is parsed by
		 * Gmail.MessageCursor#getAttachmentInfos.
		 */
		public static String joinedAttachmentsString(
				List<Gmail.Attachment> attachments) {
			StringBuilder attachmentsSb = new StringBuilder();
			for (Gmail.Attachment attachment : attachments) {
				if (attachmentsSb.length() != 0) {
					attachmentsSb.append(Gmail.ATTACHMENT_INFO_SEPARATOR);
				}
				attachmentsSb.append(attachment.toJoinedString());
			}
			return attachmentsSb.toString();
		}

	}

	/**
	 * A cursor over conversations.
	 * 
	 * "Conversation" refers to the information needed to populate a list of
	 * conversations, not all of the messages in a conversation.
	 */
	public static final class ConversationCursor extends MailCursor {

		private final LabelMap mLabelMap;

		private final int mConversationIdIndex;
		private final int mSubjectIndex;
		private final int mSnippetIndex;
		private final int mFromIndex;
		private final int mDateIndex;
		private final int mPersonalLevelIndex;
		private final int mLabelIdsIndex;
		private final int mNumMessagesIndex;
		private final int mMaxMessageIdIndex;
		private final int mHasAttachmentsIndex;
		private final int mHasMessagesWithErrorsIndex;
		private final int mForceAllUnreadIndex;

		private final TextUtils.StringSplitter mLabelIdsSplitter = newConversationLabelIdsSplitter();

		private ConversationCursor(Gmail gmail, String account, Cursor cursor) {
			super(account, cursor);
			mLabelMap = gmail.getLabelMap(account);

			mConversationIdIndex = mCursor
					.getColumnIndexOrThrow(ConversationColumns.ID);
			mSubjectIndex = mCursor
					.getColumnIndexOrThrow(ConversationColumns.SUBJECT);
			mSnippetIndex = mCursor
					.getColumnIndexOrThrow(ConversationColumns.SNIPPET);
			mFromIndex = mCursor
					.getColumnIndexOrThrow(ConversationColumns.FROM);
			mDateIndex = mCursor
					.getColumnIndexOrThrow(ConversationColumns.DATE);
			mPersonalLevelIndex = mCursor
					.getColumnIndexOrThrow(ConversationColumns.PERSONAL_LEVEL);
			mLabelIdsIndex = mCursor
					.getColumnIndexOrThrow(ConversationColumns.LABEL_IDS);
			mNumMessagesIndex = mCursor
					.getColumnIndexOrThrow(ConversationColumns.NUM_MESSAGES);
			mMaxMessageIdIndex = mCursor
					.getColumnIndexOrThrow(ConversationColumns.MAX_MESSAGE_ID);
			mHasAttachmentsIndex = mCursor
					.getColumnIndexOrThrow(ConversationColumns.HAS_ATTACHMENTS);
			mHasMessagesWithErrorsIndex = mCursor
					.getColumnIndexOrThrow(ConversationColumns.HAS_MESSAGES_WITH_ERRORS);
			mForceAllUnreadIndex = mCursor
					.getColumnIndexOrThrow(ConversationColumns.FORCE_ALL_UNREAD);
		}

		@Override
		protected void onCursorPositionChanged() {
			super.onCursorPositionChanged();
		}

		public CursorStatus getStatus() {
			Bundle extras = mCursor.getExtras();
			String stringStatus = extras.getString(EXTRA_STATUS);
			return CursorStatus.valueOf(stringStatus);
		}

		/** Retry a network request after errors. */
		public void retry() {
			Bundle input = new Bundle();
			input.putString(RESPOND_INPUT_COMMAND, COMMAND_RETRY);
			Bundle output = mCursor.respond(input);
			String response = output.getString(RESPOND_OUTPUT_COMMAND_RESPONSE);
			assert COMMAND_RESPONSE_OK.equals(response);
		}

		/**
		 * When a conversation cursor is created it becomes the active network
		 * cursor, which means that it will fetch results from the network if it
		 * needs to in order to show all mail that matches its query. If you
		 * later want to requery an older cursor and would like that cursor to
		 * be the active cursor you need to call this method before requerying.
		 */
		public void becomeActiveNetworkCursor() {
			Bundle input = new Bundle();
			input.putString(RESPOND_INPUT_COMMAND, COMMAND_ACTIVATE);
			Bundle output = mCursor.respond(input);
			String response = output.getString(RESPOND_OUTPUT_COMMAND_RESPONSE);
			assert COMMAND_RESPONSE_OK.equals(response);
		}

		/**
		 * Tells the cursor whether its contents are visible to the user. The
		 * cursor will automatically broadcast intents to remove any matching
		 * new-mail notifications when this cursor's results become visible and,
		 * if they are visible, when the cursor is requeried.
		 * 
		 * Note that contents shown in an activity that is resumed but not
		 * focused (onWindowFocusChanged/hasWindowFocus) then results shown in
		 * that activity do not count as visible. (This happens when the
		 * activity is behind the lock screen or a dialog.)
		 * 
		 * @param visible
		 *            whether the contents of this cursor are visible to the
		 *            user.
		 */
		public void setContentsVisibleToUser(boolean visible) {
			Bundle input = new Bundle();
			input.putString(RESPOND_INPUT_COMMAND, COMMAND_SET_VISIBLE);
			input.putBoolean(SET_VISIBLE_PARAM_VISIBLE, visible);
			Bundle output = mCursor.respond(input);
			String response = output.getString(RESPOND_OUTPUT_COMMAND_RESPONSE);
			assert COMMAND_RESPONSE_OK.equals(response);
		}

		/**
		 * Gets the conversation id. This is immutable. (The server calls it the
		 * original conversation id.)
		 * 
		 * @return the conversation id
		 */
		public long getConversationId() {
			return mCursor.getLong(mConversationIdIndex);
		}

		/**
		 * Returns the instructions for building from snippets. Pass this to
		 * getFromSnippetHtml in order to actually build the snippets.
		 * 
		 * @return snippet instructions for use by getFromSnippetHtml()
		 */
		public String getFromSnippetInstructions() {
			return getStringInColumn(mFromIndex);
		}

		/**
		 * Gets the conversation's subject.
		 * 
		 * @return the subject
		 */
		public String getSubject() {
			return getStringInColumn(mSubjectIndex);
		}

		/**
		 * Gets the conversation's snippet.
		 * 
		 * @return the snippet
		 */
		public String getSnippet() {
			return getStringInColumn(mSnippetIndex);
		}

		/**
		 * Get's the conversation's personal level.
		 * 
		 * @return the personal level.
		 */
		public PersonalLevel getPersonalLevel() {
			int personalLevelInt = mCursor.getInt(mPersonalLevelIndex);
			return PersonalLevel.fromInt(personalLevelInt);
		}

		/**
		 * @return a copy of the set of labels. To add or remove labels call
		 *         MessageCursor.addOrRemoveLabel on each message in the
		 *         conversation.
		 * @deprecated use getLabelIds
		 */
		@Deprecated
		public Set<String> getLabels() {
			return getLabels(getRawLabelIds(), mLabelMap);
		}

		/**
		 * @return a copy of the set of labels. To add or remove labels call
		 *         MessageCursor.addOrRemoveLabel on each message in the
		 *         conversation.
		 */
		public Set<Long> getLabelIds() {
			mLabelIdsSplitter.setString(getRawLabelIds());
			return getLabelIdsFromLabelIdsString(mLabelIdsSplitter);
		}

		/**
		 * Returns the set of labels using the raw labels from a previous
		 * getRawLabels() as input.
		 * 
		 * @return a copy of the set of labels. To add or remove labels call
		 *         MessageCursor.addOrRemoveLabel on each message in the
		 *         conversation.
		 */
		public Set<String> getLabels(String rawLabelIds, LabelMap labelMap) {
			mLabelIdsSplitter.setString(rawLabelIds);
			return getCanonicalNamesFromLabelIdsString(labelMap,
					mLabelIdsSplitter);
		}

		/**
		 * @return a joined string of labels separated by spaces. Use
		 *         getLabels(rawLabels) to convert this to a Set of labels.
		 */
		public String getRawLabelIds() {
			return mCursor.getString(mLabelIdsIndex);
		}

		/**
		 * @return the number of messages in the conversation
		 */
		public int getNumMessages() {
			return mCursor.getInt(mNumMessagesIndex);
		}

		/**
		 * @return the max message id in the conversation
		 */
		public long getMaxServerMessageId() {
			return mCursor.getLong(mMaxMessageIdIndex);
		}

		public long getDateMs() {
			return mCursor.getLong(mDateIndex);
		}

		public boolean hasAttachments() {
			return mCursor.getInt(mHasAttachmentsIndex) != 0;
		}

		public boolean hasMessagesWithErrors() {
			return mCursor.getInt(mHasMessagesWithErrorsIndex) != 0;
		}

		public boolean getForceAllUnread() {
			return !mCursor.isNull(mForceAllUnreadIndex)
					&& mCursor.getInt(mForceAllUnreadIndex) != 0;
		}
	}
}
